local path = require "fzf-lua.path"
local shell = require "fzf-lua.shell"
local utils = require "fzf-lua.utils"
local Object = require "fzf-lua.class"

local Previewer = {}

Previewer.base = Object:extend()

-- Previewer base object
function Previewer.base:new(o, opts)
  o = o or {}
  self.type = "cmd";
  self.cmd = o.cmd;
  self.args = o.args or "";
  self.opts = opts;
  return self
end

function Previewer.base:preview_window(_)
  return nil
end

function Previewer.base:preview_offset()
  --[[
    #
    #   Explanation of the fzf preview offset options:
    #
    #   ~3    Top 3 lines as the fixed header
    #   +{2}  Base scroll offset extracted from the second field
    #   +3    Extra offset to compensate for the 3-line header
    #   /2    Put in the middle of the preview area
    #
    '--preview-window '~3:+{2}+3/2''
  ]]
  if self.opts.line_field_index then
    return ("+{%d}-/2"):format(self.opts.line_field_index)
  end
end

function Previewer.base:fzf_delimiter()
  if not self.opts.line_field_index then return end
  -- set delimiter to ':'
  -- entry format is 'file:line:col: text'
  local delim = self.opts.fzf_opts and self.opts.fzf_opts["--delimiter"]
  if not delim then
    delim = "[:]"
  elseif not delim:match(":") then
    if delim:match("%[.*%]") then
      delim = delim:match("(%[.*)%]") .. ":]"
    else
      -- remove surrounding quotes
      delim = delim:match("^'?(.*)'$?") or delim
      delim = "[" .. utils.rg_escape(delim):gsub("%]", "\\]") .. ":]"
    end
  end
  return delim
end

-- Generic shell command previewer
Previewer.cmd = Previewer.base:extend()

function Previewer.cmd:new(o, opts)
  Previewer.cmd.super.new(self, o, opts)
  return self
end

function Previewer.cmd:sh_wrap(cmd, args, action, extra_args)
  return [[sh -c "]] .. string.format([[%s %s %s $(%s)"]],
    cmd, args or "", extra_args or "", action)
end

function Previewer.cmd:cmdline(o)
  o = o or {}
  o.action = o.action or self:action(o)
  return self:sh_wrap(self.cmd, self.args, o.action)
end

function Previewer.cmd:action(o)
  o = o or {}
  local act = shell.raw_action(function(items, _, _)
    -- only preview first item
    local entry = path.entry_to_file(items[1], self.opts)
    return entry.bufname or entry.path
  end, self.opts.field_index_expr or "{}", self.opts.debug)
  return act
end

-- Specialized bat previewer
Previewer.bat = Previewer.cmd:extend()

function Previewer.bat:new(o, opts)
  Previewer.bat.super.new(self, o, opts)
  self.theme = o.theme
  return self
end

function Previewer.bat:cmdline(o)
  o = o or {}
  o.action = o.action or self:action(o)
  local highlight_line = ""
  if self.opts.line_field_index then
    highlight_line = string.format("--highlight-line={%d}", self.opts.line_field_index)
  end
  return self:sh_wrap(self.cmd, self.args, o.action, highlight_line)
end

-- Specialized head previewer
Previewer.head = Previewer.cmd:extend()

function Previewer.head:new(o, opts)
  Previewer.head.super.new(self, o, opts)
  return self
end

function Previewer.head:cmdline(o)
  o = o or {}
  o.action = o.action or self:action(o)
  local lines = "--lines=-0"
  -- print all lines instead
  -- if self.opts.line_field_index then
  --   lines = string.format("--lines={%d}", self.opts.line_field_index)
  -- end
  return self:sh_wrap(self.cmd, self.args, o.action, lines)
end

-- new async_action from nvim-fzf
Previewer.cmd_async = Previewer.base:extend()

function Previewer.cmd_async:new(o, opts)
  Previewer.cmd_async.super.new(self, o, opts)
  return self
end

local grep_tag = function(file, tag)
  local line = 1
  local filepath = file
  local pattern = utils.rg_escape(tag)
  if not pattern or not filepath then return line end
  local grep_cmd = vim.fn.executable("rg") == 1
      and { "rg", "--line-number" }
      or { "grep", "-n", "-P" }
  -- ctags uses '$' at the end of short patterns
  -- 'rg|grep' does not match these properly when
  -- 'fileformat' isn't set to 'unix', when set to
  -- 'dos' we need to prepend '$' with '\r$' with 'rg'
  -- it is simpler to just ignore it completely.
  --[[ local ff = fileformat(filepath)
  if ff == 'dos' then
    pattern = pattern:gsub("\\%$$", "\\r%$")
  else
    pattern = pattern:gsub("\\%$$", "%$")
  end --]]
  -- equivalent pattern to `rg --crlf`
  -- see discussion in #219
  pattern = pattern:gsub("\\%$$", "\\r??%$")
  local cmd = utils.tbl_deep_clone(grep_cmd)
  table.insert(cmd, pattern)
  table.insert(cmd, filepath)
  local out = utils.io_system(cmd)
  if not utils.shell_error() then
    line = tonumber(out:match("[^:]+")) or 1
  else
    utils.warn(("previewer: unable to find pattern '%s' in file '%s'"):format(pattern, file))
  end
  return line
end

function Previewer.cmd_async:parse_entry_and_verify(entrystr)
  local entry = path.entry_to_file(entrystr, self.opts)
  local filepath = entry.bufname or entry.path or ""
  if self.opts._ctag and entry.line <= 1 then
    -- tags without line numbers
    -- make sure we don't already have line #
    -- (in the case the line no. is actually 1)
    local line = entry.stripped:match("[^:]+(%d+):")
    local ctag = path.entry_to_ctag(entry.stripped, true)
    if not line and ctag then
      entry.ctag = ctag
      entry.line = grep_tag(filepath, entry.ctag)
    end
  end
  local errcmd = nil
  -- verify the file exists on disk and is accessible
  if #filepath == 0 or not vim.loop.fs_stat(filepath) then
    errcmd = ('echo "%s: NO SUCH FILE OR ACCESS DENIED"'):format(
      filepath and #filepath > 0 and vim.fn.shellescape(filepath) or "<null>")
  end
  return filepath, entry, errcmd
end

function Previewer.cmd_async:cmdline(o)
  o = o or {}
  local act = shell.raw_preview_action_cmd(function(items)
    local filepath, _, errcmd = self:parse_entry_and_verify(items[1])
    local cmd = errcmd or ("%s %s %s"):format(
      self.cmd, self.args, vim.fn.shellescape(filepath))
    -- uncomment to see the command in the preview window
    -- cmd = vim.fn.shellescape(cmd)
    return cmd
  end, "{}", self.opts.debug)
  return act
end

Previewer.bat_async = Previewer.cmd_async:extend()

function Previewer.bat_async:new(o, opts)
  Previewer.bat_async.super.new(self, o, opts)
  self.theme = o.theme
  return self
end

function Previewer.bat_async:cmdline(o)
  o = o or {}
  local act = shell.raw_preview_action_cmd(function(items, fzf_lines)
    local filepath, entry, errcmd = self:parse_entry_and_verify(items[1])
    local line_range = ""
    if entry.ctag then
      -- this is a ctag without line numbers, since we can't
      -- provide the preview file offset to fzf via the field
      -- index expression we use '--line-range' instead
      local start_line = math.max(1, entry.line - fzf_lines / 2)
      local end_line = start_line + fzf_lines - 1
      line_range = ("--line-range=%d:%d"):format(start_line, end_line)
    end
    local cmd = errcmd or ("%s %s %s %s %s"):format(
      self.cmd, self.args,
      self.opts.line_field_index and
      ("--highlight-line=%d"):format(entry.line) or "",
      line_range,
      vim.fn.shellescape(filepath))
    -- uncomment to see the command in the preview window
    -- cmd = vim.fn.shellescape(cmd)
    return cmd
  end, "{}", self.opts.debug)
  return act
end

Previewer.git_diff = Previewer.base:extend()

function Previewer.git_diff:new(o, opts)
  Previewer.git_diff.super.new(self, o, opts)
  self.cmd_deleted = path.git_cwd(o.cmd_deleted, opts)
  self.cmd_modified = path.git_cwd(o.cmd_modified, opts)
  self.cmd_untracked = path.git_cwd(o.cmd_untracked, opts)
  self.pager = o.pager or opts.preview_pager
  do
    -- populate the icon mappings
    local icons_overrides = o._fn_git_icons and o._fn_git_icons()
    self.git_icons = {}
    for _, i in ipairs({ "D", "M", "R", "A", "C", "T", "?" }) do
      self.git_icons[i] =
      icons_overrides and icons_overrides[i] and
          utils.lua_regex_escape(icons_overrides[i].icon) or i
    end
  end
  return self
end

function Previewer.git_diff:cmdline(o)
  o = o or {}
  local act = shell.raw_preview_action_cmd(function(items, fzf_lines, fzf_columns)
    if not items or vim.tbl_isempty(items) then
      utils.warn("shell error while running preview action.")
      return
    end
    local is_deleted = items[1]:match(self.git_icons["D"] .. utils.nbsp) ~= nil
    local is_modified = items[1]:match("[" ..
      self.git_icons["M"] ..
      self.git_icons["R"] ..
      self.git_icons["A"] ..
      self.git_icons["T"] ..
      "]" .. utils.nbsp) ~= nil
    local is_untracked = items[1]:match("[" ..
      self.git_icons["?"] ..
      self.git_icons["C"] ..
      "]" .. utils.nbsp) ~= nil
    local file = path.entry_to_file(items[1], self.opts)
    local cmd = nil
    if is_modified then cmd = self.cmd_modified
    elseif is_deleted then cmd = self.cmd_deleted
    elseif is_untracked then cmd = self.cmd_untracked end
    if not cmd then return "" end
    local pager = ""
    if self.pager and #self.pager > 0 and
        vim.fn.executable(self.pager:match("[^%s]+")) == 1 then
      pager = "| " .. self.pager
    end
    -- with default commands we add the filepath at the end.
    -- If the user configured a more complex command, e.g.:
    -- git_diff = {
    --   cmd_modified = "git diff --color HEAD %s | less -SEX"
    -- }
    -- we use ':format' directly on the user's command, see
    -- issue #392 for more info (limiting diff output width)
    if not cmd:match("%%s") then
      cmd = cmd .. " %s"
    end
    cmd = cmd:format(vim.fn.shellescape(file.path))
    cmd = ("LINES=%d;COLUMNS=%d;FZF_PREVIEW_LINES=%d;FZF_PREVIEW_COLUMNS=%d;%s %s")
        :format(fzf_lines, fzf_columns, fzf_lines, fzf_columns, cmd, pager)
    cmd = "sh -c " .. vim.fn.shellescape(cmd)
    -- uncomment to see the command in the preview window
    -- cmd = vim.fn.shellescape(cmd)
    return cmd
    -- we need to add '--' to mark the end of command options
    -- as git icon customization may contain special shell chars
    -- which will otherwise choke our preview cmd ('+', '-', etc)
  end, "-- {}", self.opts.debug)
  return act
end

Previewer.man_pages = Previewer.base:extend()

function Previewer.man_pages:new(o, opts)
  Previewer.man_pages.super.new(self, o, opts)
  self.cmd = self.cmd or "man"
  return self
end

function Previewer.man_pages:cmdline(o)
  o = o or {}
  local act = shell.raw_preview_action_cmd(function(items)
    -- local manpage = require'fzf-lua.providers.manpages'.getmanpage(items[1])
    local manpage = items[1]:match("[^[,( ]+")
    local cmd = ("%s %s %s"):format(
      self.cmd, self.args, vim.fn.shellescape(manpage))
    -- uncomment to see the command in the preview window
    -- cmd = vim.fn.shellescape(cmd)
    return cmd
  end, "{}", self.opts.debug)
  return act
end

return Previewer
